如何搭建一个高可用的服务端渲染工程
是否一定需要引入这种技术呢?他能解决什么问题,或者能带来什么收益? 为什么要采用这种技术选型而不是其他的? 引入了这种技术后,会带来什么问题吗(比如额外的开发成本等)?
上面三个问题思考清楚之后,才能真正地去落地。上面三个问题思考清楚之后,才能真正地去落地。而有赞教育接入服务端渲染,正是为了优化H5页面的首屏内容到达时间,带来更好的用户体验(顺便利于SEO)。
一、后端模版引擎时代
后端需要写表现层的逻辑,但其实后端更应该注重服务层(和部分业务逻辑层)。当然,其实也可以让前端写JSP或FreeMarker,但从体验上来说,肯定不如写JS来的爽;
本地开发的时候,需要启动后端环境,比如Tomcat,影响开发效率,对前端也不友好;
所赋予前端的能力太少,使得前端需要的一些功能只能由后端提供,比如路由控制;
前后端耦合。
二、SPA时代
后端不需要关心表现层的逻辑,只需要注重服务层和业务逻辑层就可以了,暴露出相应的接口供前端调用。这种模式也同时实现了前后端解耦。 本地开发的时候,前端只需要启动一个本地服务,如:dev-server就可以开始开发了。 赋予了前端更多的能力,比如前端的路由控制和鉴权,比如通过SPA + 路由懒加载的模式可以带来更好的用户体验。
页面的DOM完全由js来渲染,使得大部分搜索引擎无法爬取渲染后真实的DOM,不利于SEO。
页面的首屏内容到达时间强依赖于js静态资源的加载(因为DOM的渲染由js来执行),使得在网络越差的情况下,白屏时间大幅上升。
服务端渲染的首屏直出,使得输出到浏览器的就是完备的html字符串模板,浏览器可以直接解析该字符串模版,因此首屏的内容不再依赖js的渲染。
正是因为服务端渲染输出到浏览器的是完备的html字符串,使得搜索引擎能抓取到真实的内容,利于SEO。
同时,通过基于Node和前端MVVM框架结合的服务端渲染,有着比后端模版引擎的服务端渲染更明显的优势:可以优雅降级为客户端渲染(这个后续会讲,先占个坑)。
3.1 实现
Source为我们的源代码区,即工程代码;
Universal Appliation Code和我们平时的客户端渲染的代码组织形式完全一致,只是需要注意这些代码在Node端执行过程触发的生命周期钩子不要涉及DOM和BOM对象即可;
比客户端渲染多出来的app.js、Server entry 、Client entry的主要作用为:app.js分别给Server entry 、Client entry暴露出createApp()方法,使得每个请求进来会生成新的app实例。而Server entry和Client entry分别会被webpack打包成vue-ssr-server-bundle.json和vue-ssr-client-manifest.json(这两个json文件才是有用的,app.js、Server entry 、Client entry可以抽离,开发者不感知);
Node端会根据webpack打包好的vue-ssr-server-bundle.json,通过调用createBundleRenderer生成renderer实例,再通过调用renderer.renderToString生成完备的html字符串;
Node端将render好的html字符串返回给Browser,同时Node端根据vue-ssr-client-manifest.json生成的js会和html字符串hydrate,完成客户端激活html,使得页面可交互。
3.2 优化
3.2.1 路由和代码分割
一个大的SPA,主文件js往往很大,通过代码分割可以将主文件js拆分为一个个单独的路由组件js文件,可以很大程度上减小首屏的资源加载体积,其他路由组件可以预加载。
// router.js
const Index = () => import(/* webpackChunkName: "index" */ './pages/Index.vue');
const Detail = () => import(/* webpackChunkName: "detail" */ './pages/Detail.vue');
const routes = [
{
path: '/',
component: Index
},
{
path: '/detail',
component: Detail
}
];
const router = new Router({
mode: 'history',
routes
});
// Index.vue
asyncData({ store }) {
return this.methods.dispatch(store); // 核心模块数据预取,服务端渲染
}
mounted() {
this.initOtherModules(); // 非核心模块,客户端渲染,在mounted生命周期钩子里触发
}
// page-level caching
const microCache = LRU({
max: 100,
maxAge: 1000 // 重要提示:条目在 1 秒后过期。
})
server.get('*', (req, res) => {
const hit = microCache.get(req.url)
if (hit) { // 命中缓存,直接返回页面
return res.end(hit)
}
// 服务端渲染逻辑
...
})
// component-level caching
// server.js
const LRU = require('lru-cache')
const renderer = createRenderer({
cache: LRU({
max: 10000,
maxAge: ...
})
});
// component.js
export default {
name: 'item', // 必填选项
props: ['item'],
serverCacheKey: props => props.item.id,
render (h) {
return h('div', this.item.id)
}
};
3.2.4 页面静态化
如果工程中大部分页面都是状态相关的,所以技术选型采用了服务端渲染,但有部分页面是状态无关的,这个时候用服务端渲染就有点浪费资源了。像这些状态无关的页面,完全可以通过Nginx Proxy Cache缓存到Nginx服务器,可以避免这些流量打到应用服务器集群,同时也能减少响应的时间。
3.3 降级
单个流量降级 -- 偶发的服务端渲染失败降级为客户端渲染 Disconf / Apollo配置降级 -- 分布式配置平台修改配置主动降级,比如可预见性的大流量情况下(双十一),可提前通过配置平台将整个应用集群都降级为客户端渲染 CPU阈值降级 -- 物理机 / Docker实例CPU资源占用达到阈值触发降级,避免负载均衡服务器在某些情况下给某台应用服务器导入过多流量,使得单台应用服务器的CPU负载过高 旁路系统降级 -- 旁路系统跑定时任务监控应用集群状态,集群资源占用达到设定阈值将整个集群降级(或触发集群的自动扩容) 渲染服务集群降级 -- 若渲染服务和接口服务是独立的服务,当渲染服务集群宕机,html的获取逻辑回溯到Nginx获取,此时触发客户端渲染,通过ajax调用接口服务获取数据
3.4 上线前准备
本地开发阶段:当本地的服务端渲染开发完成之后,首先需要用loadtest之类的压测工具压下性能如何,同时可以根据压测出来的数据做一些优化,如果有内存泄漏之类的bug也可以在这个阶段就能被发现。 QA性能测试阶段:当通过本地开发阶段的压测之后,我们的代码已经是经过性能优化且没有内存泄漏之类严重bug的。部署到QA性能测试环境之后,通过压真实QA环境,和原来的客户端渲染做对比,看QPS会下降多少(因为服务端渲染耗更多的CPU资源,所以QPS对比客户端渲染肯定会有下降)。 线上阶段:QA性能测试阶段压测过后,若性能指标达到原来的预期,部署到线上环境,同时可以开启一定量的压测,确保服务的可用性。
3.5 落地
3.6 效果
3.7 Q & A
因为渲染过程是在Node端,所以没有DOM和BOM对象,因此不要在常见的Vue的beforeCreate和created生命周期钩子里做涉及DOM和BOM的操作
对第三方库的要求比较高,如果想直接在Node渲染过程中调用第三方库,那这个库必须支持服务端渲染
扩展阅读
-The End-
Vol.220
有赞技术团队
为 442 万商家,150 个行业,330 亿电商交易额
提供技术支持
微商城|零售|美业 | 教育
微信公众号:有赞coder 微博:@有赞技术
技术博客:tech.youzan.com
The bigger the dream,
the more important the team